local flag_constants = {
	-- Affiliation
	AFFILIATION_MINE = 0x1,
	AFFILIATION_PARTY = 0x2,
	AFFILIATION_RAID = 0x4,
	AFFILIATION_OUTSIDER = 0x8,
	-- Reaction
	REACTION_FRIENDLY = 0x10,
	REACTION_NEUTRAL = 0x20,
	REACTION_HOSTILE = 0x40,
	-- Ownership
	CONTROL_PLAYER = 0x100,
	CONTROL_NPC = 0x200,
	-- Unit type
	TYPE_PLAYER = 0x400,
	TYPE_NPC = 0x800,
	TYPE_PET = 0x1000,
	TYPE_GUARDIAN = 0x2000,
	TYPE_OBJECT = 0x4000,

	-- Special cases (non-exclusive)
	TARGET = 0x10000,
	FOCUS = 0x20000,
	MAINTANK = 0x40000,
	MAINASSIST = 0x80000,
	RAIDTARGET1 = 0x100000,
	RAIDTARGET2 = 0x200000,
	RAIDTARGET3 = 0x400000,
	RAIDTARGET4 = 0x800000,
	RAIDTARGET5 = 0x1000000,
	RAIDTARGET6 = 0x2000000,
	RAIDTARGET7 = 0x4000000,
	RAIDTARGET8 = 0x8000000,
	NONE = 0x80000000,
}

local function decode_flags(flag)
	local result = {
		flag = flag,
	}
	for name, value in pairs(flag_constants) do
		if bit.band(flag, value) ~= 0 then
			result[name] = true
			flag = bit.band(flag, bit.bnot(value))
		end
	end
	if flag ~= 0 then
		result.remainder = flag
		print("REMAINDER", ("%x"):format(result.flag))
	end
	return result
end

local spell_schools = {
	[0] = "none",
	[1] = "physical",
	[2] = "holy",
	[4] = "fire",
	[8] = "nature",
	[16] = "frost",
	[32] = "shadow",
	[64] = "arcane",
}

local function decode_school(school)
	assert(type(school) == "number")
	return assert(spell_schools[school], "unkown spell school")
end

local energy_types = {
	[0] = "mana",
	[1] = "rage",
	[2] = "focus",
	[3] = "energy",
	[4] = "happiness",
}

local function decode_energy_type(energy)
	assert(type(energy) == "number")
	return assert(energy_types[energy], "unkown energy")
end

local function decode_guid(guid)
	if guid == "0x0000000000000000" then
		return "nil", 0, 0
	end
	local type = guid:sub(3, 5)
	if type == "F13" then
		return "npc", tonumber(guid:sub(6, 12), 16), tonumber(guid:sub(13), 16)
	elseif type == "F11" then
		return "object", tonumber(guid:sub(6, 12), 16), tonumber(guid:sub(13), 16)
	elseif type == "F14" then
		return "pet", tonumber(guid:sub(6, 12), 16), tonumber(guid:sub(13), 16)
	elseif type == "000" then
		return "pc", 0, tonumber(guid:sub(6), 16)
	else
		error("UNKNOWN UNIT TYPE: "..type)
	end
end

local function get_damage_value(s)
	return s == "nil" and 0 or tonumber(s)
end

local function get_damage_info(amount, school, resisted, blocked, absorbed, critical, glancing, crushing)
	return {
		amount = get_damage_value(amount),
		school = decode_school(tonumber(school)),
		resisted = get_damage_value(resisted),
		blocked = get_damage_value(blocked),
		absorbed = get_damage_value(absorbed),
		critical = get_damage_value(critical),
		glancing = get_damage_value(glancing),
		crushing = get_damage_value(crushing),
	}
end

local function get_heal_info(amount, critical)
	return {
		amount = get_damage_value(amount),
		critical = get_damage_value(critical),
	}
end

local function get_energize_info(amount, energy)
	return {
		amount = get_damage_value(amount),
		energy = decode_energy_type(tonumber(energy)),
	}
end

local function get_drain_info(amount, energy, extra)
	return {
		amount = get_damage_value(amount),
		energy = decode_energy_type(tonumber(energy)),
		extra = get_damage_value(amount),
	}
end

local unit_mt = {}
unit_mt.__index = unit_mt

function unit_mt:init(guid, name, flags)
	local type, typeID, spawnID = decode_guid(guid)
	self.guid = guid
	self.type = type
	self.typeID = typeID
	self.spawnID = spawnID
	self.name = assert(loadstring("return "..name))()
	self.flags = decode_flags(tonumber(flags))
	self.damage_in = {}
	self.damage_out = {}
	self.total_in = 0
	self.total_out = 0
end

function unit_mt:AddDamageInput(spell, info)
	self.total_in = self.total_in + (info.amount or 0)
end

function unit_mt:AddDamageOutput(spell, info)
	self.total_out = self.total_out + (info.amount or 0)
end

local spell_mt = {}
spell_mt.__index = spell_mt

function spell_mt:init(id, name, school)
	self.id = tonumber(id)
	self.name = assert(loadstring("return "..name))()
	self.school = decode_school(tonumber(school))
	self.types = {}
end

local data_mt = {}
data_mt.__index = data_mt

function data_mt:GetUnit(guid, name, flags)
	-- guid = tonumber(guid)
	local unit = self.units[guid]
	if not unit then
		unit = setmetatable({}, unit_mt)
		unit:init(guid, name, flags)
		self.units[guid] = unit
	else
		assert(unit.name == assert(loadstring("return "..name))())
	end
	return unit
end

function data_mt:GetSpell(type, id, name, school)
	local spell = self.spells[id]
	if not spell then
		spell = setmetatable({}, spell_mt)
		spell:init(id, name, school)
		self.spells[id] = spell
	else
		assert(spell.name == assert(loadstring("return "..name))())
	end
	spell.types[type] = true
	return spell
end

function data_mt:AddSpellDamageInfo(spell, info, source, dest)
	dest:AddDamageInput(spell, info)
	source:AddDamageOutput(spell, info)
end

function data_mt:AddSpellHealInfo(spell, info, source, dest)
--~ 	source:AddHealInput(spell, info)
--~ 	dest:AddHealOutput(spell, info)
end

function data_mt:AddSpellEnergizeInfo(spell, info, source, dest)
end

function data_mt:AddSpellMissedInfo(spell, info, source, dest)
end

function data_mt:AddSpellDrainInfo(spell, info, source, dest)
end

function data_mt:AddSwingDamageInfo(info, source, dest)
	dest:AddDamageInput(nil, info)
	source:AddDamageOutput(nil, info)
end

function data_mt:AddSwingMissedInfo(info, source, dest)
end

function data_mt:AddEnvironmentalDamageInfo(type, info, source, dest)
end

function data_mt:AddAuraAppliedInfo()
end

function data_mt:AddAuraRemovedInfo()
end

function data_mt:AddAuraStolenInfo()
end

function data_mt:AddAuraDispelledInfo()
end

function data_mt:AddSpellCastStart()
end

function data_mt:AddSpellCastSuccess()
end

function data_mt:AddSpellCastFailed()
end

function data_mt:AddSpellSummonInfo()
end

function data_mt:AddSpellInstaKillInfo()
end

function data_mt:AddSpellLeechInfo()
end

function data_mt:AddSpellDurabilityDamageInfo()
end

function data_mt:AddSpellCreateInfo()
end

function data_mt:AddSpellExtraAttack()
end

function data_mt:AddSpellInterrupt()
end

function data_mt:dummy(timestamp, event, ...)
	print(timestamp, event, ...)
end

function data_mt:UNIT_DESTROYED(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
end

function data_mt:UNIT_DIED(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	dest_unit.death = (dest_unit.death or 0) + 1
end

function data_mt:PARTY_KILL(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	if dest_unit.flags.REACTION_HOSTILE then
		source_unit.kills = (source_unit.kills or 0) + 1
	end
end

function data_mt:ENCHANT_APPLIED(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local enchant = assert(loadstring("return "..select(7, ...)))()
	local itemID = tonumber((select(8, ...)))
	local itemName = assert(loadstring("return "..select(9, ...)))()
	if source_unit ~= dest_unit then
		print("here")
	end
end

function data_mt:ENCHANT_REMOVED(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local enchant = assert(loadstring("return "..select(7, ...)))()
	local itemID = tonumber((select(8, ...)))
	local itemName = assert(loadstring("return "..select(9, ...)))()
end


function data_mt:SWING_DAMAGE(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local damage_info = get_damage_info(select(7, ...))
	self:AddSwingDamageInfo(damage_info, source_unit, dest_unit)
end

function data_mt:SWING_MISSED(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local reason = select(7, ...)
	self:AddSwingMissedInfo(reason, source_unit, dest_unit)
end

function data_mt:ENVIRONMENTAL_DAMAGE(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local type = select(7, ...)
	local damage_info = get_damage_info(select(8, ...))
	self:AddEnvironmentalDamageInfo(type, damage_info, source_unit, dest_unit)
end

function data_mt:SPELL_CAST_START(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("SPELL_CAST", select(7, ...))
	assert(dest_unit == self.units["0x0000000000000000"])
	self:AddSpellCastStart(spell, source_unit)
end

function data_mt:SPELL_CAST_SUCCESS(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("SPELL_CAST", select(7, ...))
	self:AddSpellCastSuccess(spell, source_unit, dest_unit)
end

function data_mt:SPELL_CAST_FAILED(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("SPELL_CAST", select(7, ...))
	local reason = select(10, ...)
	self:AddSpellCastFailed(spell, source_unit, dest_unit, reason)
end

function data_mt:SPELL_EXTRA_ATTACKS(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("EXTRA_ATTACK", select(7, ...))
	local count = assert(tonumber(select(10, ...)))
	self:AddSpellExtraAttack(spell, source_unit, dest_unit, count)
end

function data_mt:SPELL_INTERRUPT(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("INTERRUPT", select(7, ...))
	local cast = self:GetSpell("SPELL_CAST", select(10, ...))
	self:AddSpellInterrupt(spell, source_unit, dest_unit, cast)
end

function data_mt:SPELL_DISPEL_FAILED(timestamp, event, ...)
end

function data_mt:SPELL_SUMMON(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("SUMMON", select(7, ...))
	self:AddSpellSummonInfo(spell, source_unit, dest_unit)
end

function data_mt:SPELL_CREATE(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("CREATE", select(7, ...))
	self:AddSpellCreateInfo(spell, source_unit, dest_unit)
end

function data_mt:SPELL_INSTAKILL(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("INSTAKILL", select(7, ...))
	self:AddSpellInstaKillInfo(spell, source_unit, dest_unit)
end

function data_mt:SPELL_LEECH(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("LEECH", select(7, ...))
	local src_amount = assert(tonumber((select(10, ...))))
	local type = decode_energy_type(tonumber((select(11, ...))))
	local dst_amount = assert(tonumber(select(12, ...)))
	self:AddSpellLeechInfo(spell, source_unit, dest_unit, src_amount, type, dst_amount)
end

function data_mt:SPELL_DURABILITY_DAMAGE(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
-- @FIXME: bug in the report.
--~ 	local item = self:GetItem(select(7, ...))
--~ 	local spell = self:GetSpell("DURABILITY_DAMAGE", select(7, ...))
	local spell = self:GetSpell("DURABILITY_DAMAGE", select(10, ...), select(11, ...), 1)
--~ 	self:AddSpellDurabilityDamageInfo(spell, item, source_unit, dest_unit, type)
end

function data_mt:SPELL_AURA_APPLIED(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("AURA", select(7, ...))
	local type = select(10, ...)
	assert(source_unit == self.units["0x0000000000000000"])
	self:AddAuraAppliedInfo(spell, dest_unit, type)
end

function data_mt:SPELL_AURA_APPLIED_DOSE(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("AURA", select(7, ...))
	local type = select(10, ...)
	local count = assert(tonumber(select(11, ...)))
	assert(source_unit == self.units["0x0000000000000000"])
	self:AddAuraAppliedInfo(spell, dest_unit, type, count)
end

function data_mt:SPELL_AURA_REMOVED_DOSE(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("AURA", select(7, ...))
	local type = select(10, ...)
	local count = assert(tonumber(select(11, ...)))
	assert(source_unit == self.units["0x0000000000000000"])
	self:AddAuraRemovedInfo(spell, dest_unit, type, count)
end

function data_mt:SPELL_AURA_REMOVED(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local spell = self:GetSpell("AURA", select(7, ...))
	local type = select(10, ...)
	assert(source_unit == self.units["0x0000000000000000"])
	self:AddAuraRemovedInfo(spell, dest_unit, type)
end

function data_mt:SPELL_AURA_DISPELLED(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local dispel = self:GetSpell("DISPEL", select(7, ...))
	local spell = self:GetSpell("AURA", select(10, ...))
	local type = select(13, ...)
	self:AddAuraDispelledInfo(dispel, spell, source_unit, dest_unit, type)
end

function data_mt:SPELL_AURA_STOLEN(timestamp, event, ...)
	local source_unit = self:GetUnit(select(1, ...))
	local dest_unit = self:GetUnit(select(4, ...))
	local steal_spell = self:GetSpell("STEAL", select(7, ...))
	local stolen_spell = self:GetSpell("AURA", select(10, ...))
	local type = select(13, ...)
	self:AddAuraStolenInfo(steal_spell, stolen_spell, source_unit, dest_unit, type)
end

local function make_handlers(type)
	local damage_handler = function (self, timestamp, event, ...)
		local source_unit = self:GetUnit(select(1, ...))
		local dest_unit = self:GetUnit(select(4, ...))
		local spell = self:GetSpell(type, select(7, ...))
		local damage_info = get_damage_info(select(10, ...))
		self:AddSpellDamageInfo(spell, damage_info, source_unit, dest_unit)
	end
	local miss_handler = function (self, timestamp, event, ...)
		local source_unit = self:GetUnit(select(1, ...))
		local dest_unit = self:GetUnit(select(4, ...))
		local spell = self:GetSpell(type, select(7, ...))
		local reason = select(10, ...)
		self:AddSpellMissedInfo(spell, reason, source_unit, dest_unit)
	end
	local heal_handler = function (self, timestamp, event, ...)
		local source_unit = self:GetUnit(select(1, ...))
		local dest_unit = self:GetUnit(select(4, ...))
		local spell = self:GetSpell(type, select(7, ...))
		local heal_info = get_heal_info(select(10, ...))
		self:AddSpellHealInfo(spell, heal_info, source_unit, dest_unit)
	end
	local energize_handler = function (self, timestamp, event, ...)
		local source_unit = self:GetUnit(select(1, ...))
		local dest_unit = self:GetUnit(select(4, ...))
		local spell = self:GetSpell(type, select(7, ...))
		local energize_info = get_energize_info(select(10, ...))
		self:AddSpellEnergizeInfo(spell, energize_info, source_unit, dest_unit)
	end
	local drain_handler = function (self, timestamp, event, ...)
		local source_unit = self:GetUnit(select(1, ...))
		local dest_unit = self:GetUnit(select(4, ...))
		local spell = self:GetSpell(type, select(7, ...))
		local drain_info = get_drain_info(select(10, ...))
		self:AddSpellDrainInfo(spell, drain_info, source_unit, dest_unit)
	end
	return damage_handler, miss_handler, heal_handler, energize_handler, drain_handler
end

data_mt.DAMAGE_SHIELD, data_mt.DAMAGE_SHIELD_MISSED = make_handlers("DAMAGE_SHIELD")
data_mt.RANGE_DAMAGE, data_mt.RANGE_MISSED = make_handlers("RANGE")
data_mt.SPELL_PERIODIC_DAMAGE, data_mt.SPELL_PERIODIC_MISSED,
	data_mt.SPELL_PERIODIC_HEAL, data_mt.SPELL_PERIODIC_ENERGIZE = make_handlers("SPELL_PERIODIC")
data_mt.SPELL_DAMAGE, data_mt.SPELL_MISSED, data_mt.SPELL_HEAL,
	data_mt.SPELL_ENERGIZE, data_mt.SPELL_DRAIN = make_handlers("SPELL")

local function decompose(line)
	local month, day, hour, min, sec, r = line:match("^(%d+)/(%d+) (%d+):(%d+):([%d.]+)  (.*)$")
	local sb, sm = math.modf(sec)
	local timestamp = os.time({
		year = 2008,
		month = tonumber(month),
		day = tonumber(day),
		hour = tonumber(hour),
		min = tonumber(min),
		sec = sb,
	})
	return timestamp + sm, strsplit(",", r)
end

function data_mt:InsertLine(timestamp, event, ...)
	local handler = self[event] or self.dummy
	local status, result = pcall(handler, self, timestamp, event, ...)
	if not status then
		print("**ERROR**", result, event, ...)
	end
end

function data_mt:report()
	local f = io.open("units.txt", "w")
	for _, unit in pairs(self.units) do
		if unit.type == "pc" then
			f:write(unit.guid, "\t", unit.name, "\t", unit.death or 0, "\t", unit.kills or 0, "\t", unit.total_in or 0, "\t", unit.total_out or 0, "\n")
		end
	end
	f:close()
	
	f = io.open("spells.txt", "w")
	for _, spell in pairs(self.spells) do
		local st
		for t in pairs(spell.types) do
			if not st then
				st = tostring(t)
			else
				st = st..","..tostring(t)
			end
		end
		f:write(spell.id, "\t", st, "\t", spell.name, "\t", spell.school, "\n")
	end
	f:close()
end

function data_mt:init()
	self.units = {}
	self.spells = {}
end

local function parse(filename)
	f = io.open(filename, "r")
	local data = setmetatable({}, data_mt)
	data:init()
	for line in f:lines() do
		data:InsertLine(decompose(line))
	end
	f:close()
	return data
end

local function main(filename)
	local data = parse(filename)
	data:report()
end

if arg[0] == 'read.lua' then
	main(...)
end
